using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using System.Diagnostics;
using Xfinity.Maths.Geometry;
using System.Threading;
using System.ComponentModel;
namespace Xfinity.ContentProcessors
{
    [ContentProcessor(DisplayName = "Xfinity model content processor")]
    public class XfinityModelProcessor : ModelProcessor
    {

        class GCContentNotify
        {
            ~GCContentNotify()
            {
                if (logger != null)
                {
                    logger.LogMessage("GC fired.");
                    new GCContentNotify();
                }

            }
        }

        public static readonly int SampleCountPerVertex = 10;
        #region Process
        /// <summary>
        /// Process the model
        /// </summary>
        /// <param name="input">Input data</param>
        /// <param name="context">Context for logging</param>
        /// <returns>Model content</returns>
        public override ModelContent Process(
            NodeContent input, ContentProcessorContext context)
        {
            try
            {
                worker = new BackgroundWorker();
                worker.WorkerReportsProgress = true;
                worker.DoWork += new DoWorkEventHandler(worker_DoWork);
                worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
                worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);

                d = new Random();

                logger = context.Logger;
                logger.LogMessage("Starting ambient occlusion processing");
                // calculate ambient occlusion and save it to the output
                CalculateAmbientOcclusion(input);
                //worker.RunWorkerAsync(input);
                //CalculateAmbientOcclusion(input);
                
                logger.LogMessage("Finished ambient occlusion processing");
                // First generate tangent data because x files don't store them
                GenerateTangents(input, context);

                // Use the name of the bone for our mesh name if it is not set
                UseParentBoneNameIfMeshNameIsNotSet(input);

                // Store the current selected technique and if the texture uses alpha
                // into the mesh name for each mesh part.
                StoreEffectTechniqueInMeshName(input, context);
                ModelContent models;

                // And let the rest be processed by the default model processor
                models = base.Process(input, context) as ModelContent;
                logger = null;

                return models;
            }
            catch
            {
                if (worker != null && worker.IsBusy)
                {
                    worker.CancelAsync();
                }
                throw;

            }
            finally
            {
                logger = null;
            }
        }
        private static Thread mainThread;
        private static bool done = false;
        private static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            done = true;
            logger.LogMessage("Done occlusion testing");
        }
        private static void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            logger.LogMessage("Occlusion testing " + e.ProgressPercentage.ToString() + "% complete; " + e.UserState.ToString() + " triangles tested.");
        }

        private static void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            NodeContent input = e.Argument as NodeContent;
            CalculateAmbientOcclusion(input);
        }
        static Random d;
        static BackgroundWorker worker;
        static ContentBuildLogger logger;
        static int numberOfThreads, numberOfTriangles;
        static List<Ray> rays;

        struct LoopBounds
        {
            public int Min, Max;
        }

        /// <summary>
        /// Generates ambient oclusion data, and sets it in the vertex colour channel
        /// </summary>
        /// <param name="input">The root node.</param>
        private static void CalculateAmbientOcclusion(NodeContent input)
        {

            //populate the triangle list
            logger.LogMessage("Generating triangle list");
            LoopThroughChildContent(input);
            logger.LogMessage("Done generating triangle list.");
            logger.LogMessage(triangles.Count.ToString() + " triangles to test.");
            if (ambientOcclusion)
            {
                logger.LogMessage("Starting occlusion testing");

                GenerateTriangleOcclusionPercentages();
            }


        }
        private static void LoopThroughChildContent(NodeContent input)
        {
            List<List<Triangle>> triangleLists = new List<List<Triangle>>();

            foreach (NodeContent node in input.Children)
            {
                LoopThroughChildContent(node);
            }
            if (input is MeshContent)
            {
                MeshContent mesh = input as MeshContent;
                foreach (GeometryContent geometry in mesh.Geometry)
                {
                    List<Triangle> geometryTriangles = new List<Triangle>();
                    for (int i = 0; i < geometry.Indices.Count; i += 3)
                    {
                        Triangle t = ExtractTriangle(geometry, i);
                        geometryTriangles.Add(t);
                        if (ambientOcclusion)
                        {
                            triangles.Add(t);
                        }
                    }
                    if (ambientOcclusion)
                    {
                        geometry.Vertices.Channels.Add<float>(VertexChannelNames.Color(0), null);
                    }
                    triangleLists.Add(geometryTriangles);
                }
            }
        }
        private static Triangle ExtractTriangle(GeometryContent geometry, int i)
        {
            Triangle t = new Triangle();
            t.A = geometry.Vertices.Positions[geometry.Indices[i]];
            t.B = geometry.Vertices.Positions[geometry.Indices[i + 1]];
            t.C = geometry.Vertices.Positions[geometry.Indices[i + 2]];
            return t;
        }

        private static void GenerateTriangleOcclusionPercentages()
        {
            if (triangles == null || triangles.Count == 0)
            {
                logger.LogMessage("No triangles to test.");
                return;
            }

            rays = new List<Ray>(SampleCountPerVertex);
            for (int i = 0; i < SampleCountPerVertex; i++)
            {
                rays.Add(new Ray());
            }


            numberOfThreads = Environment.ProcessorCount;
            numberOfTriangles = triangles.Count / numberOfThreads;

            List<Thread> threads = new List<Thread>(numberOfThreads);

            for (int i = 0; i < numberOfThreads - 1; i++)
            {
                LoopBounds b = new LoopBounds();
                b.Min = i * numberOfTriangles;
                b.Max = b.Min + numberOfTriangles - 1;
                Thread thread = new Thread(DoRayTracing);
                thread.Priority = ThreadPriority.BelowNormal;
                thread.Start(b);
                threads.Add(thread);
            }
            //do the last however many triangles there are
            Thread lastThread = new Thread(DoRayTracing);
            LoopBounds lastB = new LoopBounds();
            lastB.Min = (numberOfThreads - 1) * numberOfTriangles;
            lastB.Max = triangles.Count;

            lastThread.Start(lastB);
            threads.Add(lastThread);

            Thread.CurrentThread.IsBackground = false;

            for (int i = 0; i < threads.Count; i++)
            {
                threads[i].Join();
            }

        }
        private static void GenerateRandomRays(ref Vector3 vertexPosition, ref Vector3 normal)
        {
            Vector3 dir;
            for (int i = 0; i < SampleCountPerVertex; i++)
            {
                double rho = (d.NextDouble() - .5) * 2;
                double theta = (d.NextDouble() - .5) * 2;
                double sinRho = (Math.Sin(rho));

                float x = (float)(sinRho * Math.Cos(theta));
                float y = (float)(sinRho * Math.Sin(theta));
                float z = (float)(Math.Cos(rho));
                dir = new Vector3(x, y, z);
                //dir.Normalize();
                float dot;
                Vector3.Dot(ref dir, ref normal, out dot);
                if (dot < 0)
                {
                    dir = -dir;
                }
                rays[i] = new Ray(vertexPosition, dir);
            }
        }
        private static void DoRayTracing(object bounds)
        {
            LoopBounds b = (LoopBounds)bounds;
            Triangle current;
            for (int i = b.Min; i < b.Max; i++)
            {
                current = triangles[i];
                float accumulated = 0;

                //get the normal of the currently tested triangle

                Vector3 ab, ac;

                Vector3 normal;
                current.Normal(out ab, out ac, out normal);

                Vector3 vertex;

                //test each vertex
                vertex = current.A;
                accumulated += TestVertex(ref normal, ref vertex);
                vertex = current.B;
                accumulated += TestVertex(ref normal, ref vertex);
                vertex = current.C;
                accumulated += TestVertex(ref normal, ref vertex);

                float percentage = accumulated / (SampleCountPerVertex * 3);

                worker.ReportProgress((int)(i * 100 / triangles.Count), i);
                Thread.Sleep(1);
                //TODO: multiply the vertex colour by percentage
                // OR, i could generate an ambient occlusion map
                // but alas, i have to figure out how to get the texture coords
            }

        }
        private static float TestVertex(ref Vector3 normal, ref Vector3 vertex)
        {
            Triangle potential;
            float accumulated = 0;
            Vector3 ab, ac;
            Vector3 potentialNormal;
            Ray ray;
            bool notHit;
            GenerateRandomRays(ref vertex, ref normal);
            for (int i = 0; i < SampleCountPerVertex; i++)
            {

                notHit = true;
                ray = rays[i];

                for (int triangleCount = 0; triangleCount < triangles.Count; triangleCount++)
                {

                    potential = triangles[triangleCount];

                    Vector3.Subtract(ref potential.B, ref potential.A, out ab);
                    Vector3.Subtract(ref potential.C, ref potential.A, out ac);
                    Vector3.Cross(ref ab, ref ac, out potentialNormal);

                    if (Intersects(ref potential, ref ray, ref ab, ref ac, ref potentialNormal))
                    {
                        notHit = false;
                        break;
                    }
                }
                if (notHit)
                {
                    accumulated++;
                }
            }
            return accumulated;
        }
        private static bool Intersects(ref Triangle potential, ref Ray ray, ref Vector3 ab, ref Vector3 ac, ref Vector3 n)
        {
            Vector3 qp = -ray.Direction;

            float d;
            Vector3.Dot(ref qp, ref n, out d);

            if (d <= 0)
            {
                return false;
            }

            Vector3 ap = ray.Position - potential.A;
            float t;
            Vector3.Dot(ref ap, ref n, out t);


            if (t < 0)
            {
                return false;
            }

            Vector3 e;
            float v, w;
            Vector3.Cross(ref qp, ref ap, out e);

            Vector3.Dot(ref ac, ref e, out v);
            if (v < 0 || v > d)
                return false;
            Vector3.Dot(ref ab, ref e, out w);
            w = -w;
            if (w < 0 || v + w > d)
                return false;
            return true;

        }

        private static List<Triangle> triangles = new List<Triangle>();

        public static bool ambientOcclusion = true;

        #endregion

        #region Generate tangents
        /// <summary>
        /// Generate tangents helper method, x files do not have tangents
        /// exported, we have to generate them ourselfs.
        /// </summary>
        /// <param name="input">Input data</param>
        /// <param name="context">Context for logging</param>
        private void GenerateTangents(
            NodeContent input, ContentProcessorContext context)
        {
            MeshContent mesh = input as MeshContent;
            if (mesh != null)
            {
                // Generate trangents for the mesh. We don't want binormals,
                // so null is passed in for the last parameter.
                MeshHelper.CalculateTangentFrames(mesh,
                    VertexChannelNames.TextureCoordinate(0),
                    VertexChannelNames.Tangent(0), null);
            }

            // Go through all childs
            foreach (NodeContent child in input.Children)
            {
                GenerateTangents(child, context);
            }
        }
        #endregion

        #region UseParentBoneNameIfMeshNameIsNotSet
        /// <summary>
        /// Use parent bone's name if mesh's name is not set.
        /// </summary>
        /// <param name="input"></param>
        private void UseParentBoneNameIfMeshNameIsNotSet(NodeContent input)
        {
            if (String.IsNullOrEmpty(input.Name) &&
                input.Parent != null &&
                String.IsNullOrEmpty(input.Parent.Name) == false)
                input.Name = input.Parent.Name;

            foreach (NodeContent node in input.Children)
                UseParentBoneNameIfMeshNameIsNotSet(node);
        }
        #endregion

        #region StoreEffectMaterialsAndTechniques
        /// <summary>
        /// Stores the current selected technique and if the texture uses alpha
        /// into the mesh name for each mesh part.
        /// </summary>
        private void StoreEffectTechniqueInMeshName(
            NodeContent input, ContentProcessorContext context)
        {
            MeshContent mesh = input as MeshContent;
            if (mesh != null)
            {
                foreach (GeometryContent geom in mesh.Geometry)
                {
                    EffectMaterialContent effectMaterial =
                        geom.Material as EffectMaterialContent;
                    if (effectMaterial != null)
                    {
                        if (effectMaterial.OpaqueData.ContainsKey("technique"))
                        {
                            // Store technique here! (OpaqueData["technique"] is an
                            // int32) If we have multiple mesh parts in our mesh object,
                            // there will be multiple techniques listed at the end of
                            // our mesh name.
                            input.Name =
                                input.Name + effectMaterial.OpaqueData["technique"];
                        }
                    }
                }
            }

            // Go through all childs
            foreach (NodeContent child in input.Children)
            {
                StoreEffectTechniqueInMeshName(child, context);
            }
        }
        #endregion
    }
}
